之前在開發 API 的時候,有寫一隻比較簡單的建立 QR code 的 API,但是後來都沒用到,所以這篇文章會來建立一個表單元件來使用這隻 API。
其實這個表單跟之前建立 SVG 的表單很像,只是差別在於沒有一些細部調整的功能,所以樣式的部分,大部分都可以拿之前的表單來使用。
import { useEffect } from 'react'
import { useForm, SubmitHandler } from 'react-hook-form'
import generateQrcode from '../lib/api/generateQrcode'
import { TextInput, SelectType } from './'
import useStore from '../store'
// 型別只要這個表單需要的就好
export type PngFormInputs = {
text: string
qrType: string
}
// 型別只要這個表單需要的就好
type QrCodeData = {
url?: string
phone?: string
address?: string
email?: string
}
const CreatePngForm = () => {
const { setImgSrc, setErrors } = useStore()
const {
register,
handleSubmit,
formState: { errors },
watch
} = useForm<PngFormInputs>()
const qrType = watch('qrType', 'URL')
useEffect(() => {
setImgSrc(null)
}, [qrType])
useEffect(() => {
setErrors(errors)
}, [errors])
const validationPatterns = {
URL: /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/,
電話: /^(\+?\d{1,3}[-.\s]?)?\d{10}$/,
地址: /.+/,
Email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
}
const pattern = validationPatterns[qrType as keyof typeof validationPatterns]
const fetchQrcodePng = async (formData: PngFormInputs) => {
try {
const typeMapping: {
[key in PngFormInputs['qrType']]: keyof QrCodeData
} = {
URL: 'url',
電話: 'phone',
地址: 'address',
Email: 'email'
}
const dataKey = typeMapping[formData.qrType]
const data: QrCodeData = {
[dataKey]: formData.text
}
const response = await generateQrcode.getPng(data)
const blob = new Blob([response.data], { type: 'image/png+xml' })
const objectURL = URL.createObjectURL(blob)
setImgSrc(objectURL)
} catch (_) {
console.error('Error fetching image')
}
}
const onSubmit: SubmitHandler<PngFormInputs> = async (data) => {
await fetchQrcodePng(data)
}
const hasErrors = Object.keys(errors).length > 0
return (
<form onSubmit={handleSubmit(onSubmit)} className='flex flex-col gap-y-5'>
<SelectType register={register} />
<TextInput register={register} pattern={pattern} errors={errors} />
<div className='flex justify-center mt-5'>
<button
type='submit'
className={`${
hasErrors ? 'bg-gray-400' : 'bg-green-500'
} hover:bg-green-100 text-white hover:text-slate-700 font-bold py-2 px-4 rounded text-center`}
>
產生 QR Code
</button>
</div>
</form>
)
}
export default CreatePngForm
form 表單的部分,保留 SelectType
與 TextInput
,然後將 onSubmit
改為使用 fetchQrcodePng
來取得 QR code 圖片,最後將圖片的 URL 設定到狀態中。記得要把 Blob 的型別設定為 image/png+xml
,這樣才能正確顯示圖片。
在型別的部分只要這個表單需要的就好,所以需要修改一下,並且更改一下 type 名稱,等一下比較好辨識。
現在應該可以看到 CreatePngForm
的 <SelectType />
和 <TextInput />
有紅色的底線,這是因為型別不正確,所以我們要修正一下。
既然這兩個元件是在兩邊表單都會使用到,不過兩邊的型別不太一樣,而且最大的差異是在一個是 SVG,一個是 PNG,所以就可以思考一下,要怎麼讓這兩個元件可以共用,但是又可以有不同的型別。
解決的方法有很多,這邊選擇傳入一個 props,讓元件可以根據 props 來決定型別。
SelectType
的型別修正:
import { FC } from 'react'
import { UseFormRegister } from 'react-hook-form'
import { SvgFormInputs } from './CreateSvgForm'
import { PngFormInputs } from './CreatePngForm'
type ImageType = 'SVG' | 'PNG'
interface SelectTypeProps<T extends ImageType> {
register: T extends 'SVG'
? UseFormRegister<SvgFormInputs>
: UseFormRegister<PngFormInputs>
imageType: T
}
const SelectType: FC<SelectTypeProps<ImageType>> = ({
register,
imageType
}) => {
let adjustedRegister: any
if (imageType === 'SVG') {
adjustedRegister = register as UseFormRegister<SvgFormInputs>
} else {
adjustedRegister = register as UseFormRegister<PngFormInputs>
}
return (
<div className='mb-4'>
<label className='block font-bold mb-2'>選擇 QR Code 類型</label>
<select
className='w-full p-2 border rounded focus:outline-none focus:ring focus:border-blue-300'
{...adjustedRegister('qrType')}
>
<option value='URL'>URL</option>
<option value='電話'>電話</option>
<option value='地址'>地址</option>
<option value='Email'>Email</option>
</select>
</div>
)
}
TextInput
的型別修正:
import { UseFormRegister, FieldErrors } from 'react-hook-form'
import { SvgFormInputs } from './CreateSvgForm'
import { PngFormInputs } from './CreatePngForm'
type ImageType = 'SVG' | 'PNG'
interface TextInputProps<T extends ImageType> {
register: T extends 'SVG'
? UseFormRegister<SvgFormInputs>
: UseFormRegister<PngFormInputs>
pattern: RegExp
errors: FieldErrors<SvgFormInputs>
imageType: T
}
const TextInput: FC<TextInputProps<ImageType>> = ({
register,
pattern,
errors,
imageType
}) => {
let adjustedRegister: any
if (imageType === 'SVG') {
adjustedRegister = register as UseFormRegister<SvgFormInputs>
} else {
adjustedRegister = register as UseFormRegister<PngFormInputs>
}
return (
<div className='mb-4'>
<label className='block font-bold mb-2'>輸入文字</label>
<input
type='text'
className={`w-full p-2 border rounded focus:outline-none focus:ring ${
errors?.text
? 'focus:border-red-500 border-red-500 ring-red-500'
: 'focus:border-blue-300'
}`}
{...adjustedRegister('text', { required: true, pattern })}
/>
</div>
)
}
這兩個元件的處理方式都差不多,都是使用 Type Guard 來處理,這樣就可以讓元件可以根據 imageType
來決定要使用哪個型別,只是在 TextInput
中,因為要使用 pattern
來驗證輸入的文字,所以也要將 pattern
傳入 props。
最後在 CreateSvgForm
和 CreatePngForm
中,將 SelectType
和 TextInput
傳入 props:
CreateSvgForm
:
import { useForm, SubmitHandler, UseFormRegister } from 'react-hook-form'
// 省略其他程式碼
<SelectType register={register as UseFormRegister<SvgFormInputs>} imageType="SVG" />
<TextInput register={register as UseFormRegister<SvgFormInputs>} pattern={pattern} errors={errors} imageType="SVG" />
CreatePngForm
:
import { useForm, SubmitHandler, UseFormRegister } from 'react-hook-form'
// 省略其他程式碼
<SelectType register={register as UseFormRegister<PngFormInputs>} imageType="PNG" />
<TextInput register={register as UseFormRegister<PngFormInputs>} pattern={pattern} errors={errors} imageType="PNG" />
最後就可以看到兩邊的表單都可以正常運作了。